---
title: Rhai Scripts to customize routers
subtitle: Add custom functionality directly to your router
description: Customize your Apollo Router Core or GraphOS Router functionality with Rhai scripts. Manipulate strings, process headers and more for enhanced performance.
---

You can customize your GraphOS Router or Apollo Router Core's behavior with scripts that use the [Rhai scripting language](https://rhai.rs/book/). In a Rust-based project, Rhai is ideal for performing common scripting tasks such as manipulating strings and processing headers. Your Rhai scripts can also hook into multiple stages of the router's [request handling lifecycle](#router-request-lifecycle).

## Rhai language reference

- To start learning Rhai, check out the [Rhai language reference](https://rhai.rs/book/language/) and its [example scripts](https://rhai.rs/book/start/examples/scripts.html).

- To learn about symbols and behaviors specific to customizations for GraphOS Router and Apollo Router Core, go to the [Rhai API reference for router](/graphos/reference/router/rhai).

## Use cases

Common use cases for Rhai scripts in routers include:

* Modifying the details of HTTP requests and responses. This includes requests sent from clients to your router, along with requests sent from your router to your subgraphs. You can modify any combination of the following:
    * Request and response bodies
    * Headers
    * Status codes
    * Request context
* Logging various information throughout the request lifecycle
* Performing `checkpoint`-style short circuiting of requests

## Setup

To enable Rhai scripts in your router, you add the following keys to the router's [YAML config file](/router/configuration/overview/#yaml-config-file):

```yaml title="config.yaml"
# This is a top-level key. It MUST define at least one of the two
# sub-keys shown, even if you use that subkey's default value.
rhai:
  # Specify a different Rhai script directory path with this key.
  # The path can be relative or absolute.
  scripts: "/rhai/scripts/directory"

  # Specify a different name for your "main" Rhai file with this key.
  # The router looks for this filename in your Rhai script directory.
  main: "test.rhai"
```

1. Add the `rhai` top-level key to your router's [YAML config file](/router/configuration/overview/#yaml-config-file).
    * This key **must** contain at least one of a `scripts` key or a `main` key (see the example above). 
2. Place all of your Rhai script files in a specific directory.
    * By default, the router looks in the `./rhai` directory (relative to the directory the `router` command is executed from).
    * You can override this default with the `scripts` key (see above).
3. Define a ["main" Rhai file](#the-main-file) in your router project.
    * This file defines all of the "entry point" hooks that the router uses to call into your script.
    * By default, the router looks for `main.rhai` in your Rhai script directory.
    * You can override this default with the `main` key (see above).

## The main file

Your Rhai script's main file defines whichever combination of request lifecycle hooks you want to use. Here's a skeleton `main.rhai` file that includes all available hooks and also registers all available [callbacks](#service-callbacks):

<ExpansionPanel title="Click to expand">

```rhai title="main.rhai"
// You don't need to define all of these hooks! Just define
// whichever ones your customization needs.

fn supergraph_service(service) {
  let request_callback = |request| {
      print("Supergraph service: Client request received");
  };
  
  let response_callback = |response| {
      print("Supergraph service: Client response ready to send");
  };

  service.map_request(request_callback);
  service.map_response(response_callback);
}

fn execution_service(service) {
  let request_callback = |request| {
      print("Execution service: GraphQL execution initiated");
  };
  
  let response_callback = |response| {
      print("Supergraph service: Client response assembled");
  };

  service.map_request(request_callback);
  service.map_response(response_callback);
}

// Passed an additional `subgraph` parameter that indicates the subgraph's name
fn subgraph_service(service, subgraph) {
  let request_callback = |request| {
      print(`Subgraph service: Ready to send sub-operation to subgraph ${subgraph}`);
  };
  
  let response_callback = |response| {
      print(`Subgraph service: Received sub-operation response from subgraph ${subgraph}`);
  };

  service.map_request(request_callback);
  service.map_response(response_callback);
}
```

</ExpansionPanel>

You can provide _exactly one_ main Rhai file to the router. This means that all of your customization's functionality must originate from these hook definitions.

To organize unrelated functionality within your Rhai customization, your main file can import and use symbols from any number of _other_ Rhai files (known as **modules**) in your script directory:

```rhai title="my_module.rhai"
// Module file

fn process_request(request) {
  print("Supergraph service: Client request received");
}
```

```rhai title="main.rhai"
// Main file

import "my_module" as my_mod;

fn process_request(request) {
  my_mod::process_request(request)
}

fn supergraph_service(service) {
  // Rhai convention for creating a function pointer
  const request_callback = Fn("process_request"); 
  
  service.map_request(request_callback);
}
```
<Note>

[Learn more about Rhai modules.](https://rhai.rs/book/language/modules/export.html)

</Note>

## Router request lifecycle

Before you build your Rhai script, it helps to understand how the router handles each incoming GraphQL request. During each request's execution, different **services** in the router communicate with each other as shown:

```mermaid
sequenceDiagram
    actor Client
    participant RouterService as Router<br/>Service
    participant SupergraphService as Supergraph<br/>Service
    participant ExecutionService as Execution<br/>Service
    participant SubgraphService(s) as Subgraph<br/>Service(s)

    Client->>RouterService: Sends request
    RouterService->>SupergraphService: Converts raw HTTP request<br/>to GraphQL/JSON request
    SupergraphService->>ExecutionService: Initiates query<br/>plan execution
    par
    ExecutionService->>SubgraphService(s): Initiates<br/>sub-operation
    SubgraphService(s)-->>ExecutionService: 
    and
    ExecutionService->>SubgraphService(s): Initiates<br/>sub-operation
    SubgraphService(s)-->>ExecutionService: 
    and
    ExecutionService->>SubgraphService(s): Initiates<br/>sub-operation
    SubgraphService(s)-->>ExecutionService: 
    end
    ExecutionService-->>SupergraphService: Assembles and<br/>returns response
    SupergraphService-->>RouterService: Returns GraphQL/JSON<br/>response
    RouterService-->>Client: Returns HTTP<br/>response
```

As execution proceeds "left to right" from the `RouterService` to individual `SubgraphService`s, each service passes the client's original request along to the _next_ service. Similarly, as execution continues "right to left" from `SubgraphService`s to the `RouterService`, each service passes the generated response for the client.

Your Rhai scripts can hook into any combination of the above services (if you are using the Apollo Router Core v1.30.0 and later). The scripts can modify the request, response, and/or related metadata as they're passed along.

### Service descriptions

Each service of the router has a corresponding function that a Rhai script can define to hook into that service:

<table class="field-table">
  <thead>
    <tr>
      <th>Service /<br/>Function</th>
      <th>Description</th>
    </tr>
  </thead>

<tbody>
<tr>
<td>

##### `RouterService`

`router_service`
</td>
<td>

Runs at the very beginning and very end of the HTTP request lifecycle.

For example, [JWT authentication](/router/configuration/authn-jwt) is performed within the `RouterService`.

Define `router_service` if your customization needs to interact with HTTP `context` and `headers`. It doesn't support access to the `body` property (see [this GitHub issue](https://github.com/apollographql/router/issues/3642) for details).

</td>
</tr>
<tr>
<td>

##### `SupergraphService`

`supergraph_service`
</td>
<td>

Runs at the very beginning and very end of the GraphQL request lifecycle.

Define `supergraph_service` if your customization needs to interact with the GraphQL request or the GraphQL response. For example, you can add a check for anonymous queries.

</td>
</tr>

<tr>
<td>

##### `ExecutionService`

`execution_service`
</td>
<td>

Handles initiating the execution of a query plan after it's been generated.

Define `execution_service` if your customization includes logic to govern execution (for example, if you want to block a particular query based on a policy decision).

</td>
</tr>


<tr>
<td>

##### `SubgraphService`

`subgraph_service`
</td>
<td>

Handles communication between the router and your subgraphs.

Define `subgraph_service` to configure this communication (for example, to dynamically add HTTP headers to pass to a subgraph).

Whereas other services are called once per client request, this service is called once per _subgraph_ request that's required to resolve the client's request. Each call is passed a `subgraph` parameter that indicates the name of the corresponding subgraph.

</td>
</tr>


</tbody>
</table>

Each service uses data structures for request and response data that contain:
* A context object that was created at the start of the request and is propagated throughout the entire request lifecycle. It holds:
    - The original request from the client
    - A bag of data that can be populated by plugins for communication across the request lifecycle
* Any other specific data to that service (e.g., query plans and downstream requests/responses)

### Service callbacks

Each hook in your Rhai script's [main file](#main-file) is passed a `service` object, which provides two methods: `map_request` and `map_response`. Most of the time in a hook, you use one or both of these methods to register **callback functions** that are called during the lifecycle of a GraphQL operation.

* `map_request` callbacks are called in each service as execution proceeds "to the right" from the router receiving a client request:

    ```mermaid
    graph LR;
      client(Client);
      client --> router(router_service);
      subgraph Router
      router --> supergraph(supergraph_service);
      supergraph --> execution(execution_service);
      execution --> subs_a(subgraph_service);
      execution --> subs_b(subgraph_service);
      end;
      subs_a --> sub_a(Subgraph A);
      subs_b --> sub_b(Subgraph B);
      class client,sub_a,sub_b secondary;
    ```
    
    These callbacks are each passed the current state of the client's `request` (which might have been modified by an earlier callback in the chain). Each callback can modify this `request` object directly.
    
    Additionally, callbacks for `subgraph_service` can access and modify the sub-operation request that the router will send to the corresponding subgraph via `request.subgraph`.
    
    For reference, see the fields of [`request`](/graphos/reference/router/rhai/#request-interface).
    
* `map_response` callbacks are called in each service as execution proceeds back "to the left" from subgraphs resolving their individual sub-operations:

    ```mermaid
    graph RL;
      client(Client);
      subgraph Router
      execution(execution_service);
      supergraph(supergraph_service);
      router(router_service);
      subs_a(subgraph_service);
      subs_b(subgraph_service);
      end;
      sub_a(Subgraph A);
      sub_b(Subgraph B);
      sub_a --> subs_a;
      sub_b --> subs_b;
      subs_a --> execution;
      subs_b --> execution;
      execution --> supergraph;
      supergraph --> router;;
      router --> client;
      class client,sub_a,sub_b secondary;
    ```
    
    First, callbacks for `subgraph_service` are each passed the `response` from the corresponding subgraph.
    
    Afterward, callbacks for `execution_service`, `supergraph_service` and then `router_service` are passed the combined `response` for the client that's assembled from all subgraph `response`s.
    


## Example scripts

In addition to the examples below, see more examples in the router repo's [examples directory](https://github.com/apollographql/router/tree/main/examples). Rhai-specific examples are listed in `README.md`.

### Handling incoming requests

This example illustrates how to register router request handling.

```rhai

// At the supergraph_service stage, register callbacks for processing requests
fn supergraph_service(service) {
    const request_callback = Fn("process_request"); // This is standard Rhai functionality for creating a function pointer
    service.map_request(request_callback); // Register the callback
}

// Generate a log for each request
fn process_request(request) {
    log_info("this is info level log message");
}
```

### Manipulating headers and the request context

This example manipulates headers and the request context:

```rhai
// At the supergraph_service stage, register callbacks for processing requests and
// responses.
fn supergraph_service(service) {
    const request_callback = Fn("process_request"); // This is standard Rhai functionality for creating a function pointer
    service.map_request(request_callback); // Register the request callback
    const response_callback = Fn("process_response"); // This is standard Rhai functionality for creating a function pointer
    service.map_response(response_callback); // Register the response callback
}

// Ensure the header is present in the request
// If an error is thrown, then the request is short-circuited to an error response
fn process_request(request) {
    log_info("processing request"); // This will appear in the router log as an INFO log
    // Verify that x-custom-header is present and has the expected value
    if request.headers["x-custom-header"] != "CUSTOM_VALUE" {
        log_error("Error: you did not provide the right custom header"); // This will appear in the router log as an ERROR log
        throw "Error: you did not provide the right custom header"; // This will appear in the errors response and short-circuit the request
    }
    // Put the header into the context and check the context in the response
    request.context["x-custom-header"] = request.headers["x-custom-header"];
}

// Ensure the header is present in the response context
// If an error is thrown, then the response is short-circuited to an error response
fn process_response(response) {
    log_info("processing response"); // This will appear in the router log as an INFO log
    // Verify that x-custom-header is present and has the expected value
    if response.context["x-custom-header"] != "CUSTOM_VALUE" {
        log_error("Error: we lost our custom header from our context"); // This will appear in the router log as an ERROR log
        throw "Error: we lost our custom header from our context"; // This will appear in the errors response and short-circuit the response
    }
}
```

<Caution>

**Accessing a non-existent header throws an exception**. 

To safely check for the existence of a header before reading it, use `request.headers.contains("header-name")`. 

When using `contains` within `subgraph_service`, you must assign your headers to a temporary local variable. Otherwise, `contains` throws an exception that it "cannot mutate" the original request.

For example, the following `subgraph_service` function checks whether the `x-custom-header` exists before processing it.

```rhai
// Ensure existence of header before processing
fn subgraph_service(service, subgraph){
    service.map_request(|request|{
        // Reassign to local variable, as contains cannot modify request
        let headers = request.headers; 
        if headers.contains("x-custom-header") {
            // Process existing header
        }
    });
}
```

</Caution>

### Converting cookies to headers

This example converts cookies into headers for transmission to subgraphs. There is a complete working example (with tests) of this in the [examples/cookies-to-headers directory](https://github.com/apollographql/router/tree/main/examples/cookies-to-headers).

```rhai
// Call map_request with our service and pass in a string with the name
// of the function to callback
fn subgraph_service(service, subgraph) {
    // Choose how to treat each subgraph using the "subgraph" parameter.
    // In this case we are doing the same thing for all subgraphs
    // and logging out details for each.
    print(`registering request callback for: ${subgraph}`); // print() is the same as using log_info()
    const request_callback = Fn("process_request");
    service.map_request(request_callback);
}

// This will convert all cookie pairs into headers.
// If you only wish to convert certain cookies, you
// can add logic to modify the processing.
fn process_request(request) {
    print("adding cookies as headers");

    // Find our cookies
    let cookies = request.headers["cookie"].split(';');
    for cookie in cookies {
        // Split our cookies into name and value
        let k_v = cookie.split('=', 2);
        if k_v.len() == 2 {
            // trim off any whitespace
            k_v[0].trim();
            k_v[1].trim();
            // update our headers
            // Note: we must update subgraph.headers, since we are
            // setting a header in our subgraph request
            request.subgraph.headers[k_v[0]] = k_v[1];
        }
    }
}
```

## Hot reloading

The router "watches" your `rhai.scripts` directory (along with all subdirectories), and it initiates an interpreter reload whenever it detects one of the following changes:

 * Creation of a new file with a `.rhai` suffix
 * Modification or deletion of an existing file with a `.rhai` suffix

The router attempts to identify any errors in your scripts before applying changes. If errors are detected, the router logs them and continues using its _existing_ set of scripts.

<Note>

Whenever you make changes to your scripts, check your router's log output to make sure they were applied.

</Note>

## Limitations

Rhai customization is best suited for simple modifications such as altering headers, modifying context values, or lightweight payload transformations.

For more advanced functionality — including network requests, writing to disk, using Rust crates injecting custom span data, or accessing external data — use YAML configuration or [external co-processing](/router/customizations/coprocessor/).

### Global variables

The router's `Rhai` interface can simulate closures: https://rhai.rs/book/language/fn-closure.html

However, and this is an important restriction:

"
The [anonymous function](https://rhai.rs/book/language/fn-anon.html) syntax, however, automatically captures [variables](https://rhai.rs/book/language/variables.html) that are not defined within the current scope, but are defined in the external scope – i.e. the scope where the [anonymous function](https://rhai.rs/book/language/fn-anon.html) is created.
"

Thus it's not possible for a `Rhai` closure to reference a global variable. For example: this kind of thing might be attempted:

```rhai
fn supergraph_service(service){
    let f = |request| {
        let v = Router.APOLLO_SDL;
        print(v);
    };
    service.map_request(f);
}
```
Note: `Router` is a global variable.

That won't work and you'll get an error something like: `service callback failed: Variable not found: Router (line 4, position 17)`

There are two workarounds. Either:

1. Create a local copy of the global that can be captured by the closure:

```rhai
fn supergraph_service(service){
    let v = Router.APOLLO_SDL;
    let f = |request| {
        print(v);
    };
    service.map_request(f);
}
```

Or:

2. Use a function pointer rather than closure syntax:

```rhai
fn supergraph_service(service) {
    const request_callback = Fn("process_request");
    service.map_request(request_callback);
}

fn process_request(request) {
    print(`${Router.APOLLO_SDL}`);
}
```

### Avoiding deadlocks

The router requires its Rhai engine to implement the [sync feature](https://rhai.rs/book/start/features.html) to guarantee data integrity within the router's multi-threading execution environment. This means that [shared values](https://rhai.rs/book/language/fn-closure.html?highlight=deadlock#data-races-in-sync-builds-can-become-deadlocks) within Rhai might cause a deadlock.

This is particularly risky when using closures within callbacks while referencing external data. Take particular care to avoid this kind of situation by making copies of data when required. The [examples/surrogate-cache-key directory](https://github.com/apollographql/router/tree/main/examples/surrogate-cache-key) contains an example of this, where "closing over" `response.headers` would cause a deadlock. To avoid this, a local copy of the required data is obtained and used in the closure.

### Services

Callbacks of `router_service` cannot access the body of a request or response. At the router service stage, a request or response body is an opaque sequence of bytes.

## Debugging

### Understanding errors

If there is a syntax error in a Rhai script, the router will log an error message at startup mentioning the `apollo.rhai` plugin. The line number in the error message describes where the error was _detected_, not where the error is present. For example, if you're missing a semicolon at the end of line 10, the error will mention line 11 (once Rhai realizes the mistake).

### Syntax highlighting

Syntax highlighting can make it easier to spot errors in a script. We recommend using the online [Rhai playground](https://rhai.rs/playground/stable/) or using VS Code with the [Rhai extension](https://marketplace.visualstudio.com/items?itemName=rhaiscript.vscode-rhai).

### Logging

For tracking down runtime errors, insert [logging](/graphos/reference/router/rhai#logging) statements to narrow down the issue.

### Error logging behavior

When a Rhai callback function throws an error, the router automatically logs the error details to help with debugging. However, the logging behavior depends on whether you provide a custom error body:

#### Errors without custom body

When a callback throws a simple error (a string), the router logs the full error details at <code>ERROR</code> level:

```rhai
fn supergraph_service(service) {
    service.map_request(|request| {
        throw "Something went wrong"; // This error will be logged
    });
}
```

The log output includes:
- HTTP status code (defaults to <code>500</code>)
- Error message
- Line number and position where the error occurred

#### Errors with custom body

When a callback throws an error with a custom GraphQL response body, the router does _not_ log the error details:

```rhai
fn supergraph_service(service) {
    service.map_request(|request| {
        // This error will NOT be logged (to avoid duplication)
        throw #{
            status: 403,
            body: #{
                errors: [#{
                    message: "Unauthorized",
                    extensions: #{
                        code: "UNAUTHORIZED"
                    }
                }]
            }
        };
    });
}
```

## Additional resources

You can use the Apollo Solutions [router extensibility load testing repository](https://github.com/apollosolutions/router-extensibility-load-testing) to load test Rhai scripts as well as router configurations.
The Apollo Solutions [Rhai test repository](https://github.com/apollosolutions/rhai-test) is an experimental CLI tool for running unit tests against your router Rhai scripts.

<SolutionsNote />
